/*
 * SoapUI, Copyright (C) 2004-2017 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.security.scan;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.MalformedXmlAttributeConfig;
import com.eviware.soapui.config.MalformedXmlConfig;
import com.eviware.soapui.config.SecurityScanConfig;
import com.eviware.soapui.config.StrategyTypeConfig;
import com.eviware.soapui.model.ModelItem;
import com.eviware.soapui.model.iface.MessageExchange;
import com.eviware.soapui.model.security.SecurityCheckedParameter;
import com.eviware.soapui.model.testsuite.TestCaseRunner;
import com.eviware.soapui.model.testsuite.TestProperty;
import com.eviware.soapui.model.testsuite.TestStep;
import com.eviware.soapui.security.SecurityTestRunContext;
import com.eviware.soapui.security.SecurityTestRunner;
import com.eviware.soapui.security.ui.MalformedXmlAdvancedSettingsPanel;
import com.eviware.soapui.support.types.StringToStringMap;
import com.eviware.soapui.support.xml.XmlObjectTreeModel;
import com.eviware.soapui.support.xml.XmlObjectTreeModel.AttributeXmlTreeNode;
import com.eviware.soapui.support.xml.XmlObjectTreeModel.XmlTreeNode;
import com.eviware.soapui.support.xml.XmlUtils;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlOptions;

import javax.swing.JComponent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public class MalformedXmlSecurityScan extends AbstractSecurityScanWithProperties {

    public static final String TYPE = "MalformedXmlSecurityScan";
    public static final String NAME = "Malformed XML";
    private Map<SecurityCheckedParameter, ArrayList<String>> parameterMutations = new HashMap<SecurityCheckedParameter, ArrayList<String>>();
    private boolean mutation;
    private MalformedXmlConfig malformedXmlConfig;
    private MalformedXmlAttributeConfig malformedAttributeConfig;
    private MalformedXmlAdvancedSettingsPanel advancedSettingsPanel;

    public MalformedXmlSecurityScan(TestStep testStep, SecurityScanConfig config, ModelItem parent, String icon) {
        super(testStep, config, parent, icon);
        if (config.getConfig() == null || !(config.getConfig() instanceof MalformedXmlConfig)) {
            initMalformedXmlConfig();
        } else {
            malformedXmlConfig = ((MalformedXmlConfig) config.getConfig());
            malformedAttributeConfig = malformedXmlConfig.getAttributeMutation();
        }
    }

    /**
     * Default malformed xml configuration
     */
    protected void initMalformedXmlConfig() {
        getConfig().setConfig(MalformedXmlConfig.Factory.newInstance());
        malformedXmlConfig = (MalformedXmlConfig) getConfig().getConfig();

        malformedXmlConfig.addNewAttributeMutation();

        // init default configuration
        malformedXmlConfig.setInsertNewElement(true);
        malformedXmlConfig.setNewElementValue("<xml>xml <joke> </xml> </joke>");
        malformedXmlConfig.setChangeTagName(true);
        malformedXmlConfig.setLeaveTagOpen(true);
        malformedXmlConfig.setInsertInvalidCharacter(true);

        malformedAttributeConfig = malformedXmlConfig.getAttributeMutation();
        malformedAttributeConfig.setMutateAttributes(true);
        malformedAttributeConfig.setInsertInvalidChars(true);
        malformedAttributeConfig.setLeaveAttributeOpen(true);
        malformedAttributeConfig.setAddNewAttribute(true);
        malformedAttributeConfig.setNewAttributeName("newAttribute");
        malformedAttributeConfig.setNewAttributeValue("XXX");
    }

    @Override
    protected void execute(SecurityTestRunner runner, TestStep testStep, SecurityTestRunContext context) {
        try {
            StringToStringMap paramsUpdated = update(testStep, context);
            MessageExchange message = (MessageExchange) testStep.run((TestCaseRunner) runner, context);
            createMessageExchange(paramsUpdated, message, context);
        } catch (XmlException e) {
            SoapUI.logError(e, "[MalformedXmlSecurityScan]XPath seems to be invalid!");
            reportSecurityScanException("Property value is not XML or XPath is wrong!");
        } catch (Exception e) {
            SoapUI.logError(e, "[MalformedXmlSecurityScan]Property value is not valid xml!");
            reportSecurityScanException("Property value is not XML or XPath is wrong!");
        }
    }

    protected StringToStringMap update(TestStep testStep, SecurityTestRunContext context) throws XmlException,
            Exception {
        StringToStringMap params = new StringToStringMap();

        if (parameterMutations.size() == 0) {
            mutateParameters(testStep, context);
        }

        if (getExecutionStrategy().getStrategy() == StrategyTypeConfig.ONE_BY_ONE) {
            /*
			 * Idea is to drain for each parameter mutations.
			 */
            for (SecurityCheckedParameter param : getParameterHolder().getParameterList()) {
                if (parameterMutations.containsKey(param)) {
                    if (parameterMutations.get(param).size() > 0) {
                        TestProperty property = testStep.getProperties().get(param.getName());
                        String value = context.expand(property.getValue());
                        if (param.getXpath() == null || param.getXpath().trim().length() == 0) {
                            // no xpath ignore
                        } else {
                            // no value, do nothing.
                            if (value == null || value.trim().equals("")) {
                                continue;
                            }
                            // XmlObjectTreeModel model = new XmlObjectTreeModel(
                            // property.getSchemaType().getTypeSystem(),
                            // XmlObject.Factory.parse( value ) );
                            XmlObjectTreeModel model = new XmlObjectTreeModel(property.getSchemaType().getTypeSystem(),
                                    XmlUtils.createXmlObject(value));
                            XmlTreeNode[] nodes = model.selectTreeNodes(context.expand(param.getXpath()));
                            StringBuffer buffer = new StringBuffer(value);
                            for (int cnt = 0; cnt < nodes.length; cnt++) {
                                // find right node
                                // this finds where node that needs updateing begins
                                int start = value.indexOf("<" + nodes[cnt].getNodeName()); // keeps
                                // node
                                // start
                                int cnt2 = 0;
                                // if have more than one node that matches xpath, find
                                // next one.
                                while (cnt2 < cnt) {
                                    start = value.indexOf("<" + nodes[cnt].getNodeName(), start + 1);
                                    cnt2++;
                                }
                                // get node xml
                                String nodeXml = getXmlForNode(nodes[cnt]);
                                // find end of target xml node
                                int end = value.indexOf("<" + nodes[cnt].getNodeName(), start + 1);
                                if (end <= 0) {
                                    if (nodeXml.endsWith("</" + nodes[cnt].getDomNode().getNodeName() + ">")) {
                                        end = value.indexOf("</" + nodes[cnt].getDomNode().getNodeName() + ">")
                                                + ("</" + nodes[cnt].getDomNode().getNodeName() + ">").length();
                                    } else {
                                        end = value.indexOf(">", value.indexOf("/", start));
                                    }
                                }
                                if (end <= 0 || end <= start) {
                                    break;
                                }
                                // replace node with right value
                                buffer.replace(start, end + 1, parameterMutations.get(param).get(0));
                            }
                            params.put(param.getLabel(), parameterMutations.get(param).get(0));
                            parameterMutations.get(param).remove(0);

                            testStep.getProperties().get(param.getName()).setValue(buffer.toString());
                        }

                        break;
                    }
                }
            }
        } else {
            for (TestProperty property : testStep.getPropertyList()) {

                String value = context.expand(property.getValue());
                if (XmlUtils.seemsToBeXml(value)) {
                    StringBuffer buffer = new StringBuffer(value);
                    XmlObjectTreeModel model = null;
                    // model = new XmlObjectTreeModel(
                    // property.getSchemaType().getTypeSystem(),
                    // XmlObject.Factory.parse( value ) );
                    model = new XmlObjectTreeModel(property.getSchemaType().getTypeSystem(),
                            XmlUtils.createXmlObject(value));
                    for (SecurityCheckedParameter param : getParameterHolder().getParameterList()) {
                        if (param.getXpath() == null || param.getXpath().trim().length() == 0) {
                            if (parameterMutations.containsKey(param)) {
                                testStep.getProperties().get(param.getName())
                                        .setValue(parameterMutations.get(param).get(0));
                                params.put(param.getLabel(), parameterMutations.get(param).get(0));
                                parameterMutations.get(param).remove(0);
                            }
                        } else {
                            // no value, do nothing.
                            if (value == null || value.trim().equals("")) {
                                continue;
                            }
                            if (param.getName().equals(property.getName())) {
                                XmlTreeNode[] nodes = model.selectTreeNodes(context.expand(param.getXpath()));
                                if (parameterMutations.containsKey(param)) {
                                    if (parameterMutations.get(param).size() > 0) {
                                        for (int cnt = 0; cnt < nodes.length; cnt++) {
                                            // find right node
                                            // keeps node start
                                            int start = value.indexOf("<" + nodes[cnt].getNodeName());
                                            int cnt2 = 0;
                                            while (cnt2 < cnt) {
                                                start = value.indexOf("<" + nodes[cnt].getNodeName(), start + 1);
                                                cnt2++;
                                            }
                                            String nodeXml = getXmlForNode(nodes[cnt]);
                                            int end = value.indexOf("<" + nodes[cnt].getNodeName(), start + 1);
                                            if (end <= 0) {
                                                if (nodeXml.endsWith("</" + nodes[cnt].getDomNode().getNodeName() + ">")) {
                                                    end = value.indexOf("</" + nodes[cnt].getDomNode().getNodeName() + ">");
                                                } else {
                                                    end = value.indexOf(">", value.indexOf("/", start));
                                                }
                                            }
                                            if (end <= 0 || end <= start) {
                                                break;
                                            }
                                            buffer.replace(start, end + 1, parameterMutations.get(param).get(0));
                                        }
                                        params.put(param.getLabel(), parameterMutations.get(param).get(0));
                                        parameterMutations.get(param).remove(0);
                                    }
                                }
                            }
                        }
                    }
                    if (model != null) {
                        property.setValue(buffer.toString());
                    }
                }

            }
        }
        return params;
    }

    protected void mutateParameters(TestStep testStep, SecurityTestRunContext context) throws XmlException,
            IOException {
        mutation = true;
        // for each parameter
        for (SecurityCheckedParameter parameter : getParameterHolder().getParameterList()) {
            if (parameter.isChecked()) {
                TestProperty property = getTestStep().getProperties().get(parameter.getName());
                // check parameter does not have any xpath
                if (parameter.getXpath() == null || parameter.getXpath().trim().length() == 0) {
					/*
					 * parameter xpath is not set ignore than ignore this parameter
					 */
                } else {
                    // we have xpath but do we have xml which need to mutate
                    // ignore if there is no value, since than we'll get exception
                    if (!(property.getValue() == null && property.getDefaultValue() == null)) {
                        // get value of that property
                        String value = context.expand(property.getValue());

                        // we have something that looks like xpath, or hope so.

                        // XmlObjectTreeModel model = new XmlObjectTreeModel(
                        // property.getSchemaType().getTypeSystem(),
                        // XmlObject.Factory.parse( value ) );
                        XmlObjectTreeModel model = new XmlObjectTreeModel(property.getSchemaType().getTypeSystem(),
                                XmlUtils.createXmlObject(value));
                        XmlTreeNode[] nodes = model.selectTreeNodes(context.expand(parameter.getXpath()));

                        if (nodes.length > 0 && !(nodes[0] instanceof AttributeXmlTreeNode)) {
                            if (!parameterMutations.containsKey(parameter)) {
                                parameterMutations.put(parameter, new ArrayList<String>());
                            }
                            parameterMutations.get(parameter).addAll(mutateNode(nodes[0], value));
                        }
                    }

                }
            }
        }
    }

    protected Collection<? extends String> mutateNode(XmlTreeNode node, String xml) throws IOException {

        ArrayList<String> result = new ArrayList<String>();
        String nodeXml = getXmlForNode(node);
        // insert new element
        if (malformedXmlConfig.getInsertNewElement()) {
            StringBuffer buffer = new StringBuffer(nodeXml);
            if (nodeXml.endsWith("</" + node.getDomNode().getNodeName() + ">")) {
                buffer.insert(nodeXml.indexOf(">") + 1, malformedXmlConfig.getNewElementValue());
            } else {
                buffer.delete(nodeXml.lastIndexOf("/"), nodeXml.length());
                buffer.append(">" + malformedXmlConfig.getNewElementValue() + "</" + node.getDomNode().getNodeName() + ">");
            }
            result.add(buffer.toString());
        }
        // change name
        if (malformedXmlConfig.getChangeTagName()) {
            String original = node.getNodeName();

            if (original.toUpperCase().equals(original)) {
                result.add(nodeXml.replaceAll(original, original.toLowerCase()));
            } else if (original.toLowerCase().equals(original)) {
                result.add(nodeXml.replaceAll(original, original.toUpperCase()));
            } else {
                StringBuffer buffer = new StringBuffer();
                // kewl
                for (char ch : original.toCharArray()) {
                    if (Character.isUpperCase(ch)) {
                        buffer.append(Character.toLowerCase(ch));
                    } else {
                        buffer.append(Character.toUpperCase(ch));
                    }
                }
                result.add(nodeXml.replaceAll(original, buffer.toString()));

                // add '_' before upper case and make uppercase lowercase
                // just start tag change
                buffer = new StringBuffer();
                for (char ch : original.toCharArray()) {
                    if (Character.isUpperCase(ch)) {
                        buffer.append("_").append(Character.toLowerCase(ch));
                    } else {
                        buffer.append(ch);
                    }
                }
                result.add(nodeXml.replaceAll(original, buffer.toString()));
            }

        }
        // leave tag open
        if (malformedXmlConfig.getLeaveTagOpen()) {
            if (nodeXml.endsWith("</" + node.getDomNode().getNodeName() + ">")) {
                // cut end tag
                StringBuffer buffer = new StringBuffer(nodeXml);
                buffer.delete(buffer.indexOf("</" + node.getDomNode().getNodeName() + ">"), buffer.length());
                result.add(buffer.toString());

                // cut start tag
                buffer = new StringBuffer(nodeXml);
                buffer.delete(0, buffer.indexOf(">") + 1);
                result.add(buffer.toString());

                // cut start tag and remove '/' from end tag
                buffer = new StringBuffer(nodeXml);
                buffer.delete(0, buffer.indexOf(">") + 1);
                buffer.delete(buffer.indexOf("</" + node.getDomNode().getNodeName() + ">") + 1,
                        buffer.indexOf("</" + node.getDomNode().getNodeName() + ">") + 2);
                result.add(buffer.toString());
            } else {
                // remove '/>' from end of tag
                StringBuffer buffer = new StringBuffer(nodeXml);
                buffer.delete(nodeXml.lastIndexOf("/"), nodeXml.length());
                result.add(buffer.toString());
            }
        }
        if (malformedXmlConfig.getInsertInvalidCharacter()) {
            for (char ch : new char[]{'<', '>', '&'}) {
                StringBuffer buffer = new StringBuffer(nodeXml);
                if (nodeXml.endsWith("</" + node.getDomNode().getNodeName() + ">")) {
                    buffer.insert(buffer.indexOf("</" + node.getDomNode().getNodeName() + ">"), ch);
                } else {
                    buffer.delete(nodeXml.lastIndexOf("/"), nodeXml.length());
                    buffer.append('>').append(ch).append("</").append(node.getDomNode().getNodeName()).append(">");
                }
                result.add(buffer.toString());
            }
        }

        // mutate attributes
        if (malformedAttributeConfig.getMutateAttributes()) {
            if (malformedAttributeConfig.getAddNewAttribute()) {
                if (malformedAttributeConfig.getNewAttributeName().trim().length() > 0) {
                    // insert new attribute just after node tag
                    StringBuffer buffer = new StringBuffer(nodeXml);
                    buffer.insert(node.getNodeName().length() + 1, " " + malformedAttributeConfig.getNewAttributeName()
                            + "=" + "\"" + malformedAttributeConfig.getNewAttributeValue() + "\" ");
                    result.add(buffer.toString());
                }
            }
            if (malformedAttributeConfig.getInsertInvalidChars()) {
                if (node.getDomNode().hasAttributes()) {
                    for (char ch : new char[]{'"', '\'', '<', '>', '&'}) {
                        // add it at beggining of attribute value
                        StringBuffer buffer = new StringBuffer(nodeXml);
                        buffer.insert(buffer.indexOf("=") + 3, ch);
                        result.add(buffer.toString());
                    }
                }
            }
            if (malformedAttributeConfig.getLeaveAttributeOpen()) {
                if (node.getDomNode().hasAttributes()) {
                    StringBuffer buffer = new StringBuffer(nodeXml);
                    buffer.delete(buffer.indexOf("=") + 1, buffer.indexOf("=") + 2);
                    result.add(buffer.toString());
                }
            }
        }
        return result;
    }

    private String getXmlForNode(XmlTreeNode nodes) {
        XmlOptions options = new XmlOptions();
        options.setSaveOuter();
        options.setSavePrettyPrint();

        String xml = nodes.getXmlObject().xmlText(options);

        return XmlUtils.removeUnneccessaryNamespaces(xml);
    }

    @Override
    public String getConfigDescription() {
        return "Configures Malformed XML Security Scan";
    }

    @Override
    public String getConfigName() {
        return "Malformed XML Security Scan";
    }

    @Override
    public String getHelpURL() {
        return "http://soapui.org/Security/malformed-xml.html";
    }

    @Override
    public String getType() {
        return TYPE;
    }

    @Override
    protected boolean hasNext(TestStep testStep, SecurityTestRunContext context) {
        boolean hasNext = false;
        if ((parameterMutations == null || parameterMutations.size() == 0) && !mutation) {
            if (getParameterHolder().getParameterList().size() > 0) {
                hasNext = true;
            } else {
                hasNext = false;
            }
        } else {
            for (SecurityCheckedParameter param : parameterMutations.keySet()) {
                if (parameterMutations.get(param).size() > 0) {
                    hasNext = true;
                    break;
                }
            }
        }
        if (!hasNext) {
            parameterMutations.clear();
            mutation = false;
        }
        return hasNext;
    }

    @Override
    protected void clear() {
        parameterMutations.clear();
        mutation = false;
    }

    @Override
    public JComponent getAdvancedSettingsPanel() {
        if (advancedSettingsPanel == null) {
            advancedSettingsPanel = new MalformedXmlAdvancedSettingsPanel(malformedXmlConfig);
        }

        return advancedSettingsPanel.getPanel();
    }

    @Override
    public void release() {
        if (advancedSettingsPanel != null) {
            advancedSettingsPanel.release();
        }

        super.release();
    }

}
